You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Name the component, not its element, in RenderError — and report mount, host-root and ErrorBoundary-caught render errors - #1323
ReactorEventSource.RenderError (EventId 9) reported the element type for class components — ComponentElement`1 for every Component<T, TProps>(…), ComponentElement for Component<T>() — so winapp devtools logs and the overlay error badge could not name <Counter>. Mount-time and host-root render errors emitted no event at all, and neither did any render error an ErrorBoundary caught. This PR reports the component's type name on every path where a component's Render() throws, including inside a boundary, with exactly one event per throw.
Spec 044 §6.2.1 (PII) is respected: payloads carry developer-authored type names only.
What changed
ComponentNames.For(Component? instance, Element? element) (internal, src/Reactor/Core/Diagnostics/ComponentNames.cs), precedence: the live instance's type → ComponentElement.ComponentType → the element type (FuncElement / MemoElement). Non-generic names are exactly Type.Name, as before; a generic component renders as ItemList<Int32> instead of ItemList`1. A nested generic shows only its own type arguments. Reflects only Name / GetGenericArguments, so it is AOT/trim-safe. Generic names are cached per type in a ConditionalWeakTable, so collectible or hot-reloaded types are never rooted.
RenderError uses that name, and is now emitted from:
the update path, which no longer depends on the Render keyword being on;
the three mount paths — Component, Func, Memo (new);
the root render catch blocks (component and render function) in ReactorHost and ReactorHostControl (new), and those hosts' outer catch when the root's effect flush throws (new). Reconcile / Cleanup failures have no failing component and are not reported;
inside an ErrorBoundary (new): each component catch gains a sibling clause, when (_errorBoundaryDepth > 0), that emits RenderError and rethrows unchanged. The throw site is the only place that knows which descendant threw, and the boundary does not emit again, so there is exactly one event per throw.
Still RenderError (EventId 9, Errors keyword, Error level, same payload). Existing subscribers pick up the new emissions with no change.
ComponentRenderStart / Stop, EffectsFlush*, ComponentUnmount (including the ErrorBoundary rollback unmount from [Feature] Let apps replace the built-in error fallback that shows the full exception text #1291) and the reconciler's ILogger messages use the same helper, so every event agrees on the name. ComponentUnmount now computes the name only when its Lifecycle keyword is enabled.
SwallowedError is unchanged (its payload is category / operation / exception type; no call site passes a component).
Guide: source-mapping (template + compiled + diagram) — the component-name paragraph, the attribution diagram label/alt text, and the RenderError row. CHANGELOG.md[Unreleased] → Fixed entry. advanced guide: how the ETW event relates to RenderErrorHandler. Spec-044 swallowed-error ledger rows for every render-error catch this PR touches.
Test plan
New unit tests tests/Reactor.Tests/Diagnostics/ComponentNamesTests.cs (12). Mutation check: reverting to element naming / raw Type.Name fails 4 of the original 9; GenericName_IsBuiltOnceAndReused covers the per-type name cache.
New selftest fixtures tests/Reactor.AppTests.Host/SelfTest/Fixtures/RenderErrorNamesFixture.cs, subscribed to Errors only:
RenderErrorNames_ErrorBoundaryCatchIsReported — inside a boundary on mount (class, func, memo, and a throw that crosses an intermediate component) and on update: fallback shown, reported exactly once, by the thrower's name. Also an exception whose Message getter throws, and an explicitly thrown OutOfMemoryException on every boundary arm (class/func/memo mount and update).
dotnet run --project tests/Reactor.AppTests.Host --no-build -c Debug -p:Platform=x64 -- --self-test --filter "RenderError" — passed, 0 failures (at the merge 4075055bf), together with main's RenderErrorHandler_* and ErrorBoundary* fixtures.
Mutation check of the new selftest oracles: removing the FuncElement mount emission and the ReactorHostControl root-function emission fails RenderErrorNames_Mount_FuncComponentReported and RenderErrorNames_HostControl_RootFunc_Reported (2 failures); reading ex.Message in EmitRenderError fails RenderErrorNames_Boundary_ThrowingMessage_ReportedOnce; restoring the OOM/SO carve-out on the boundary arms fails all 4 boundary *OutOfMemory_ReportedOnce checks; limiting the report-and-rethrow arms to boundaries again fails the 4 non-boundary OOM checks. Restored afterwards.
dotnet test tests/Reactor.Tests -p:Platform=x64 — passed: 14435 total, 14371 succeeded, 64 skipped, 0 failed (at d8496be71, on origin/main1e8c6b196). dotnet test tests/Reactor.DocPipeline.Tests -p:Platform=x64 — passed 479/479. SwallowedErrorAuditTests — passed 11/11.
dotnet build src/Reactor/Reactor.csproj --no-restore -c Release -p:Platform=x64 and dotnet build tests/Reactor.AppTests.Host --no-restore -c Release -p:Platform=x64 — passed, 0 errors (warnings-as-errors on). The full dotnet build Reactor.slnx --no-restore -c Release could not be completed locally: dotnet restore Reactor.slnx fails with NU1301 (TLS handshake to nuget.org) for the vs-reactor projects in this environment; relying on CI's Build solution job for that gate.
REACTOR_SELFTEST_SHARD=1/2 dotnet test tests/Reactor.SelfTests -p:Platform=x64 at 78a593d2e — passed: 930 total, 929 succeeded, 1 skipped (the intentional SelfTestVerdict_OnlySkips_PositiveControl), 0 failed.
No public API added; reactor.api.txt unchanged.
Merged origin/main (00cd48920: Let apps replace the built-in render-error fallback #1311RenderErrorHandler, Add pr-lifecycle contributor skill #1346). Conflicts at every render catch site were resolved keeping both intents. Main's BuildInTreeFallback / ShowErrorFallback dispatch and !RenderErrorDispatch.IsPropagating(ex) filters are unchanged. RenderError is emitted before the handler runs, so it fires once per throw whatever the handler returns. The report-and-rethrow arms also let a declined propagation pass, since its throw site already reported it. New fixture RenderErrorNames_RenderErrorHandlerOutcomesReportOnce covers an app fallback (child and root), a handled Propagate(), and a declined Propagate() escaping through an ErrorBoundary; each is reported once, by name. Mutation: dropping the IsPropagating filter reports the declined case twice (FuncElement, NestedDecliningComponent). RenderError.ComponentName (the handler API) keeps main's values. API index regenerated with mur --regen-api: unchanged. Copilot's round on the merge raised 3 findings, fixed in 9ee41eaa4. (1) The [Feature] Let apps replace the built-in error fallback that shows the full exception text #1291 rollback ComponentUnmount used the old naming; it now goes through ComponentNames + the keyword guard, covered by RenderErrorNames_BoundaryRollbackUnmountNamesComponent. (2, 3) Host-root effect failures reached the handler with no RenderError; they now emit, covered by RenderErrorNames_*RootEffect* / *RootFuncEffect*. All 5 new checks fail with the fixes reverted. The docs scope the event to component Render()/effect failures, and the spec-044 ledger records the hosts' outer catches (LedgerSiteTotal 136 → 138). The next round, fixed in 0469209dc: (a) both hosts read _rootComponent after app code ran, so a root that Mount()s a replacement and then throws was attributed to the replacement. The root is now captured before Render() / FlushEffects(), and the same instance names the event and the handler's RenderError.ComponentName; covered by RenderErrorNames_*RootReplaced* (4 checks, all red on the mutant). (b) "Previously missed": ComponentNames cut an array argument down to its generic name (GenericList<List<int>[]> → GenericList<List>); it now formats element type and rank, covered by the unit test ArrayArguments_KeepTheirElementTypeAndRank (red on the mutant). Merged origin/main again (fix(datagrid): keep inline editor text aligned with the display cell #1349, Enable IDE0330 and move lock gates to System.Threading.Lock #1351): the CHANGELOG conflict was resolved by keeping both Fixed entries. The next round found that the func/memo mount arms hardcoded FuncElement / MemoElement, while those records are non-sealed and update/unmount name derived records by their own type. Fixed by routing them through ComponentNames.For, covered by RenderErrorNames_Mount_Derived*ElementNamed (red on the mutant). Release builds of src/Reactor, tests/Reactor.AppTests.Host and tests/Reactor.Tests are clean under the newly enabled IDE0330. The next round asked for generic derived element records to use the generic format (DerivedFunc<Int32>, not DerivedFunc1). Fixed in b585258and covered by the unit testGenericDerivedElements_UseTheGenericFormat(red on the mutant). Mergedorigin/mainagain (#1347, #1318) with no conflicts. #1318's new publicReactorHostInfo.RootComponentName` deliberately uses its own fully qualified format and is untouched.
After the merge: dotnet test tests/Reactor.Tests -p:Platform=x64passed 14579 total, 0 failed, 64 skipped (at 3cc699958). Release builds of src/Reactor, tests/Reactor.AppTests.Host and tests/Reactor.Tests are clean, and the RenderError / ErrorBoundary / HostDiagnostics selftests have 0 failures. dotnet test tests/Reactor.DocPipeline.Tests -p:Platform=x64passed 493/493. Release src/Reactor builds clean. dotnet test tests/Reactor.SelfTests -p:Platform=x64 (full): 1785 total, 1777 passed, 5 skipped, 3 failed. The failures are Issue487_ScrollViewOffsetRestoredAfterRunMutation, ItemsView_Parking_PendingBringThenKeyedReset and ItemsView_Parking_VariableHeightReset; a clean origin/main00cd48920 worktree fails them identically on this machine, so they are environmental and unrelated to this PR. CI is the gate.
Copilot review round 1 (3 comments): two said RenderError leaks ex.Message — not the case, the ReactorEventSource.RenderError sink writes string.Empty for the message (TASK-064); replied and noted it on EmitRenderError. One asked to cache generic names — done in 78a593d2e (ConditionalWeakTable per type), and both ComponentUnmount call sites now skip computing the name when the Lifecycle keyword is off. Round 2: "Previously missed" — CHANGELOG entry; added under [Unreleased] → Fixed. Round 3: the new catches were missing from the spec-044 swallowed-error ledger (docs/specs/044/swallowed-error-audit.md); recorded 12 sites (8 Keep / user-callback isolation fallback catches incl. the host roots, 4 PromoteEvent report-and-rethrow arms), distribution re-derived, LedgerSiteTotal 124 → 136. Round 4: don't read the virtual ex.Message on the error path (a throwing override would replace the original exception) — fixed, selftest added. Round 5 "Previously missed": the boundary arms' OOM/SO carve-out let an explicitly thrown OutOfMemoryException be recovered silently — carve-out removed (the boundary's own catch takes everything), selftests added; and the source-mapping diagram label + alt text still said Component.GetType().Name — updated in template, guide and both SVG copies. Round 6 "Previously missed": outside a boundary an explicitly thrown OOM/SO skipped the fallback arm and was recovered by the host's outer catch without an event — the report-and-rethrow arm now takes OOM/SO anywhere (they still propagate exactly as before); selftests cover class/func/memo mount and update. Round 7: Findings: None, nothing previously missed.
Pre-PR review with the repo's pr-review skill: 0 critical / 0 high. Four medium test-coverage gaps (func/memo mount, ReactorHost root render function, ReactorHostControl roots) fixed by the third commit. One low finding dismissed: "ComponentNames.For(Type) duplicates Reconciler.Mount.FriendlyTypeName" — FriendlyTypeName prints the declaring chain (Outer.Inner<T>) for copy-pasteable handler diagnostics, while component event names deliberately keep Type.Name for nested types so existing trace names stay byte-identical; sharing would need a mode switch that changes one or the other. Multi-model cross-check agreed.
Risk / breaking changes
Listeners now receive previously silent errors.RenderError also fires for render errors at mount time (class, function, memo), in the host root (ReactorHost and ReactorHostControl), and inside an ErrorBoundary (once, at the throw site, even though the app recovered and shows the fallback). Errors-keyword consumers will see more events.
The component name changes from ComponentElement / ComponentElement`1 to the component's type name. That is the fix, but greps keyed on the old string will stop matching.
Generic component names change from Foo`1 to Foo<Int32> in all component events. Non-generic names are byte-identical.
No new public API; same event, same payload shape.
Follow-up (not in this PR): the pending ComponentRendered event change should use the same ComponentNames helper; that will be folded into whichever of the two lands second.
This PR improves Reactor diagnostics by consistently reporting component names (not ComponentElement / ComponentElement\1) across render tracing, unmount, and RenderErroremission paths, including behavior insideErrorBoundary`.
This externally visible diagnostics change is missing from CHANGELOG.md. The repo requires notable changes under [Unreleased] in a Keep-a-Changelog bucket with the originating reference (CHANGELOG.md:3,13-24), and this change both adds previously silent RenderError emissions and changes names consumed by existing listeners. Please add a Fixed entry citing issue #1321 (and spec 044 §6.2.1).
Artifact sizes for 671fed2 vs the base branch (12d0816).
Packages (compressed .nupkg)
Artifact
base
PR
Δ
Microsoft.UI.Reactor.nupkg
1.79 MB
1.80 MB
+1.62 KB (+0.09%)
⚠️
Microsoft.UI.Reactor.Advanced.nupkg
493.4 KB
493.3 KB
-13 B (0.00%)
≈
Microsoft.UI.Reactor.Devtools.nupkg
288.2 KB
288.2 KB
+5 B (0.00%)
≈
Assemblies in Microsoft.UI.Reactor
Artifact
base
PR
Δ
Reactor.Analyzers.dll
386.0 KB
386.0 KB
+0 B (0.00%)
≈
Reactor.dll
2.52 MB
2.52 MB
+1.50 KB (+0.06%)
⚠️
Reactor.Localization.Generator.dll
16.0 KB
16.0 KB
+0 B (0.00%)
≈
Reactor.Wrappers.Abstractions.dll
10.5 KB
10.5 KB
+0 B (0.00%)
≈
Reactor.Wrappers.Generator.dll
99.5 KB
99.5 KB
+0 B (0.00%)
≈
Assemblies in Microsoft.UI.Reactor.Advanced
Artifact
base
PR
Δ
Reactor.Advanced.dll
1.00 MB
1.00 MB
+0 B (0.00%)
≈
Assemblies in Microsoft.UI.Reactor.Devtools
Artifact
base
PR
Δ
Microsoft.UI.Reactor.Devtools.dll
788.0 KB
788.0 KB
+0 B (0.00%)
≈
✅ smaller / ⚠️ larger / ≈ within noise. Sizes come from a Release dotnet pack on the CI runner: packages are the compressed .nupkg download size, assemblies the uncompressed DLL inside it. workflow run.
RenderError reported the ELEMENT type for class components: "ComponentElement`1"
for every Component<T, TProps>(...) and "ComponentElement" for Component<T>(),
unless the Render keyword was also enabled (the update path only borrowed the
real name from the render-span bookkeeping). The mount path and the hosts' root
render emitted no RenderError at all, so a component that throws on its first
render, or a throwing root, was invisible to an Errors-only listener.
- ComponentNames.For(instance, element): live instance type, else
ComponentElement.ComponentType, else the element type (FuncElement /
MemoElement). Non-generic names are unchanged (Type.Name); generic components
render "ItemList<Int32>" instead of "ItemList`1". Type names only (PII-safe).
- RenderError now uses it on the update path and is emitted on the mount paths
(Component / Func / Memo) and for ReactorHost / ReactorHostControl roots.
- ComponentRenderStart/Stop, EffectsFlush and ComponentUnmount, plus the
reconciler's ILogger messages, use the same name.
- SwallowedError carries no component name (category / operation / exception
type only): unchanged.
- Unit tests for the naming (mutation-checked); selftest fixture
RenderErrorNames_ComponentTypeOnEveryPath for mount/update/root emission;
source-mapping guide.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
A component render error inside an ErrorBoundary propagates to the boundary,
which logs and renders its fallback. The component's own catch is filtered on
_errorBoundaryDepth == 0, so no RenderError was emitted: the user saw a
fallback while an inspector (overlay error badge, devtools logs) saw nothing.
- Each component render catch (update path, and Component / Func / Memo mount)
gains a sibling clause for _errorBoundaryDepth > 0 that emits RenderError with
the failing component's ComponentNames name and rethrows unchanged. The throw
site is the only place that knows which descendant threw; the boundary does
not emit again, so it is exactly one event per throw.
- Same event (RenderError, EventId 9, Errors, Error level, same payload): every
existing subscriber picks it up with no change, and the event's meaning (a
component's Render() threw) is unchanged.
- Selftest fixture RenderErrorNames_ErrorBoundaryCatchIsReported (mount and
update inside a boundary, once by name); source-mapping guide row.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Coverage for 671fed2 vs the base branch (12d0816) — unit + selftest merged.
Metric
base
PR
Δ
Line
86.52%
86.65%
+0.13 pp
✅
Branch
77.36% (967/1250)
77.44% (968/1250)
+0.08 pp
≈
✅ higher / ⚠️ lower / ≈ within noise. Δ is in percentage points; coverage is unit + selftest merged (Debug x64) on the CI runner. Cobertura reports attached to the workflow run as artifacts.
The sink redacts the message, and an app exception whose Message override
throws would otherwise replace the original exception on the
ErrorBoundary rethrow path.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Boundary-caught OutOfMemoryException remains invisible to Errors listeners
src/Reactor/Core/Reconciler.Mount.cs:871
An ErrorBoundary catches every Exception in MountErrorBoundary, including an explicitly thrown OutOfMemoryException, but this filter bypasses the only throw-site RenderError emission for those exceptions. The boundary then renders its fallback, so the error is handled yet remains invisible to Errors listeners, contradicting the new “including errors an ErrorBoundary catches” contract. This boundary arm should report every exception that the enclosing boundary will catch (or the boundary itself must be narrowed consistently).
The enclosing ErrorBoundary catches every Exception, but this filter skips RenderError for an explicitly thrown OutOfMemoryException before the boundary recovers with its fallback. As a result, this function-component mount path still has a boundary-caught render error with no event, contrary to the documented behavior.
This issue also appears on line 967 of the same file.
Update boundary misses RenderError for OutOfMemoryException
src/Reactor/Core/Reconciler.cs:2173
UpdateErrorBoundary catches every Exception, so an explicitly thrown OutOfMemoryException bypasses this emission and is nevertheless recovered by the boundary. That produces no RenderError for a boundary-caught update error, contrary to the new event contract; this throw-site arm should cover the same exception set as the enclosing boundary.
Update image alt text to reflect ComponentNames formatting
The image alt text immediately below still says component attribution flows via Component.GetType().Name, which would produce the raw Foo`1 form this paragraph now says is not emitted. Update the template to name the ComponentNames formatting path, then regenerate the compiled guide so the accessible description matches the documented behavior.
…ibution alt text
The report-and-rethrow arms excluded OutOfMemoryException and
StackOverflowException, but the boundary's own catch takes everything, so an
explicitly thrown OOM was recovered without a RenderError. The source-mapping
diagram and its alt text still named Component.GetType().Name.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…arguments
Capture the root component before app code runs in both hosts, so a root that
Mount()s a replacement from Render() or an effect and then throws is reported
(on RenderError and to the RenderErrorHandler) as itself, not the replacement.
ComponentNames now formats array arguments (List<Int32>[]) instead of cutting
them to the bare generic name.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
FuncElement and MemoElement are non-sealed; the mount arms hardcoded the base
names while update and unmount use ComponentNames.For, so a derived record had
two names. Route the mount arms through the helper too.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
main is red at 22f2679: #1318 added ReactorHostRegistry's object gate after
#1351 enabled IDE0330, so the Release build fails. Same one-line change #1351
applied to every other gate.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…ComponentNames
Keeps #1323's RenderError emission (before the fallback, including ErrorBoundary-caught throws) alongside ComponentRendered: the boundary arms main added now also report the render. Folds in the cross-dependency change: ComponentRendered names components with ComponentNames.For (generic classes as Foo<Int32>, FuncElement/MemoElement fallbacks), for child components and host roots, so it agrees with RenderError. Adds ComponentRendered_GenericNames_MatchComponentNames.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Conflicts kept both sides: CHANGELOG entries; NeedsTag keeps the Native AOT
call-site-only skip and adds #1318's component-boundary tag; the generator test
stub has both the static-info and root-mount-site members; one combined
InternalsVisibleTo comment. API index regenerated. K's tests now pass the
column that SourceLocation requires positionally.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…component names
Folds in the integration follow-up (2495eb4b7) now that #1318 and #1323 are
on main: a root render function's hooks= resolves from its recorded mount
site, and owner/mounts/root names forward to ComponentNames.For. Also adopts
System.Threading.Lock for the static-facts gate (IDE0330, now enforced in
Release).
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…ohfi-reactor-component-diagnostics
- Hosts: keep #1328/#1323's root lifecycle (RetireRoot, deferred
retirement, _releaseReplacedTreeOnNullRender, RootRenderDiagnostics)
alongside the diagnostics rendered-root publication. The published root
is snapshotted at the start of the pass (beside _renderingRoot), so a
Mount() from the root's own Render() does not relabel the tree that pass
commits; RetireRoot clears it once the root is retired (immediately or
in the deferred pass), so a retired root is never described or written.
- Selftests: the remount gap now describes no root (the old one is
retired at Mount); new mid-pass Mount case; mutation-checked.
- SourceLocation's new required ColumnNumber in a unit test.
- CHANGELOG keeps both entries; API index regenerated (unchanged).
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
… control, declared names, hook names (Native AOT) (#1342)
* Stamp the call-site column into SourceLocation
The source-map interceptor generator already knows the exact column of each
DSL call; the runtime SourceLocation only exposed file + line, so an inspector
could not tell Row(Button("a"), Button("b")) apart or anchor a comment on
the exact call.
- SourceLocation gains a non-positional int ColumnNumber { get; init; }
(1-based, 0 = unknown) and a (FilePath, LineNumber, ColumnNumber) ctor.
The 2-arg ctor, Deconstruct, ToString and ToShortString are unchanged and
the struct stays 16 bytes.
- The generator stamps the column of the invoked method's name (the open
paren's when name and paren sit on different lines, because the line
follows the paren for [CallerLineNumber] parity), and the argument
expression's column for argument-position stamps. Column takes part in the
incremental-model equality so a sideways-only edit regenerates.
- Generator driver tests and live column tests (oracle = the test file's own
text); spec 010 amendment, source-mapping guide, API index.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Add ComponentRendered event with render reason and instance id
Inspectors (winapp devtools "highlight updates", why-did-render) need to know
when each component renders and why. The existing ComponentRenderStart/Stop
pair is Informational timing only: it skips mount and the host root, reports
just "self"/"parent", and carries no instance identity, so an overlay cannot
tell which on-screen component to flash.
- ReactorEventSource.ComponentRendered, EventId 40, Level Verbose, keywords
Render | RenderDetail (new, 0x4000). Payload: componentName (CLR type name),
componentId (process-unique long), reason (mount | state | props | context |
parent | hotReload | forced), elapsedMicroseconds. Written with
WriteEventCore (no boxing/params array). No PII: type names and fixed tokens.
- Emitted from the reconciler's mount and update paths and from both hosts'
root render. Reasons come from the existing memo/force/self-trigger state.
- ReactorTrace.GetComponentControl(long) / TryGetComponentId(UIElement, out
long) resolve ids to the component's wrapper (or the host's root content).
The registry is weak, populated only while the event is enabled, and
dropped on unmount so a pooled wrapper is never misattributed.
- Disabled cost: one EventSource.IsEnabled check per component render
(measured ~1 ns, 0 B), plus a long on each ComponentNode.
- Unit tests for the wire contract, keyword/level gating, classification and
registry; a selftest fixture for the reconciler wiring; diagnostics and
perf-instrumentation docs; API index.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Drop forced GC and Path.Combine from the ComponentRendered/column tests
Prune is now exercised by expiring a weak entry through a test hook rather
than GC.Collect(), which also makes it deterministic; ColumnTests joins a
fixed relative path instead of Path.Combine with segments.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Share host-root ComponentRendered bookkeeping; gate adoption tracking
ReactorHost and ReactorHostControl now hold one RootRenderDiagnostics
(id, first-render flag, Reset/TraceRendered/TrackContent) instead of
parallel fields and methods, so the two hosts cannot drift. The
realized-replacement adoption path only touches the registry while
ComponentRendered is enabled; the next traced render re-tracks the id.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Report ComponentRendered on every error path; changelog
- A component whose Render() throws inside an ErrorBoundary now reports the
render (mount and update paths) before the exception reaches the boundary.
- A host pass that fails after the root rendered maps the root id to the
error panel the outer catch installs, in both hosts.
- Retargeting an id clears the previous control's reverse slot.
- CHANGELOG entry for SourceLocation.ColumnNumber, ComponentRendered and the
ReactorTrace lookups (issue #1326).
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Never leave a ComponentRendered id on a control that is off screen
- Mount paths map a component's id to its wrapper only after the subtree has
mounted; a descendant that throws into an enclosing ErrorBoundary discards
the wrapper without unmounting it.
- Reconciler.ForgetComponentDiagnostics drops every tracked component's id;
Reconciler.Dispose and both hosts' ShowErrorFallback (which replace the
tree without unmounting it) call it.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Report reason context for function components whose context changed
A function component has no memo gate, so it reported parent even when a
context value it consumes is what changed. Classify that case as context,
computed only while ComponentRendered is traced.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Attribute a hot-reload hook-order retry to hotReload, not forced
When a hot-reload edit changes the root's hook order, the host resets the
root and retries outside the hot-reload pass, so ComponentRendered reported
that retry (root and children) as forced. The host now marks the pending
force pass as a hot-reload retry on the reconciler (diagnostics only; render
behaviour is unchanged) and both the root and child classification honour it.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Tighten ComponentRendered reasons and error coverage; regenerate docs
- Trace a render that threw into an ErrorBoundary for every exception the
boundary can catch (drop the OOM/SO exclusion on the trace-and-rethrow
clauses).
- A propless component whose consumed context changed reports context, not
parent; props components keep props-over-context precedence.
- Function-component updates read the event gate once.
- SourceLocation stays 16 bytes on 64-bit only; x86 grows from 8 to 12.
The size test and spec 010 say so.
- source-mapping frontmatter and diagram alt text mention the column;
architecture-overview and reconciliation recompiled for the moved snippet.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Forget a component id on DetachReactorState; show the column in the diagram
DetachReactorState, the full-detach teardown, now drops the control's
ComponentRendered id in both directions like unmount does. The
source-mapping attribution diagram labels per-element attribution as
file:line:column and shows a column in the CallSite example.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Reuse the ComponentRendered gate snapshot on the update path
The function-component context classification reads the event gate into
traceEnabled; the render path now reuses that snapshot instead of reading
the gate again, so a listener enabling between the two reads cannot turn a
context-driven render into one reported as parent.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Keep the host-root id honest across root swaps and tracing off
- ReactorHost.Mount(Func) clears the component root, as ReactorHostControl
does; the render loop checks the component root first, so the old root kept
rendering under the new root's fresh mount id.
- The root's content mapping is updated on every content change: tracked while
ComponentRendered is enabled, dropped while it is off, so it never names a
control that left the screen.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Use a using declaration for the root-mapping fixture's subscription
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Run the outgoing component root's cleanups when switching to a function root
Mount(Func) now retires the previous component root's RenderContext (its
effect cleanups run, as on Dispose) before clearing it, in both hosts;
otherwise a root UseEffect subscription or timer outlived the switch and was
no longer reachable by Dispose.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Roll back boundary-discarded mappings; retire any outgoing root on remount
- Mounts inside an ErrorBoundary record the component ids they map; a boundary
that catches (on mount or update) forgets every mapping made since it
started, so a healthy sibling mounted before a throwing one does not stay
mapped to a discarded wrapper.
- Both hosts retire whichever root is active (component or function context:
cleanups run, both slots cleared, diagnostics reset) before either Mount
overload installs the new one.
- Document that a host root reports state for host-requested re-renders too.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Keep the active root alive when it is re-mounted
Mount(component) with the instance already mounted now just requests a
render. Retiring it ran its effect cleanups and then reused the same
RenderContext, whose unchanged effects would never be scheduled again.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Reset a retired root component's hooks, not just its cleanups
RetireRoot now resets the outgoing component's hook state
(RenderContext.ResetHookState: cleanups run and hook cells are dropped), so
mounting that caller-owned instance again later is a fresh mount: its effects
are scheduled again and old state does not survive. ResetForHotReload
delegates to the same method.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Report hot-reload hook-order attempts and null-returning roots
- A Render() aborted by a hot-reload hook-order change (child in the
reconciler, root in either host) now emits ComponentRendered before the
reset; the retry reports itself, timed on its own.
- A root whose Render() returns null is reported too (nothing to reconcile,
but it rendered); a host with no root still reports nothing.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Report a throwing render before its fallback, and for every propagated exception
- The fallback catch emits ComponentRendered before building the error
fallback, so the fallback's time is not counted and a fallback that throws
cannot swallow the event.
- The trace-and-rethrow clause now covers every exception the fallback catch
does not handle (an ErrorBoundary-bound one, or a fatal one propagating to
the host), on mount and update.
- The update reason is computed once per render.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Track the root's hot-reload retry in RootRenderDiagnostics
Both hosts mark the root's hook-order retry on RootRenderDiagnostics, so the
root reports it as hotReload whether or not the host's hot-reload pass is a
forced one. ReactorHost (whose pass is forced and whose force flag is still
pending) keeps also marking the reconciler so its children follow;
ReactorHostControl's passes are not forced, so it no longer sets that
reconciler flag, whose comment overstated what it did there.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Publish ReactorSource diagnostics on controls; declared names and hook names
Lets an out-of-process inspector (WinUI DevTools over XamlDiagnostics) show
Reactor's component tree, call sites, keys, declared names and hook names with
no managed agent in the app, so Native AOT apps can be inspected too.
K: ReactorDiagnostics.SourceProperty (attached string DP, "ReactorSource")
- Set on EVERY realized control in diagnostics mode (mount, update, skip-arm
call-site refresh, hot-reload migration, component wrappers, host roots),
not only the controls Reactor tags. Value v1:
v=1|at=path:line[:col][|rel=root|0]|owner=C|element=Kind[|mounts=C][|root=C]
[|key=k][|name=n][|hooks=i:name@line;...] (% -> %25, | -> %7C)
- at= is never an absolute developer path (XAML ms-appx parity): relative to
the project directory, else to the solution root (rel=root), else the file
name only (rel=0); deterministic /_/ paths are kept. CallSite is unchanged.
- Gating: Reactor.DevtoolsSupport switch AND REACTOR_DIAGNOSTICS=1. Call sites
guard on the new cached feature switch ReactorFeatures.DevtoolsSupported,
so trimmed/AOT builds without it carry no publishing code (AOT Hello-World
trim assertions extended; positive control with the switch on keeps it).
- $(ReactorDiagnostics) MSBuild property: default true in Debug; true turns
the switch on (Trim=true) and implies ReactorSourceMap; explicit settings,
and a project-declared switch over the Debug default, win.
I: declared names and hook names (source-map generator, Debug/source-map only)
- The generator records the identifier an element is assigned to (locals,
fields, properties, assignments, expression-bodied members/local functions,
through fluent chains) -> SourceLocation.DeclaredName and name=.
- Per class Render() / render function: ordered Use* calls with the variable
they are stored in and their line -> hooks=; slot indices exact for
single-slot built-ins (runtime-verified table) and straight-line custom hooks
in the project, '?' after unknown or conditional hooks.
- Emitted as a module initializer that registers a fill callback; tables are
built lazily on first lookup.
Tests: generator snapshots (names, hooks, roots), live source-map tests,
format/escaping/relative-path/gating unit tests, hook slot table guard,
build-targets evaluation tests, ReactorSource selftest fixture (not run).
Docs: diagnostics, source-mapping guide; threat model §7.7.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* ReactorSource: fix lost owners after GC; publish only when the value can change
Measuring diagnostics mode (K) on a 1k-50k control benchmark found a
correctness bug and most of its cost in the publisher's per-control side table.
Bug: the last published owner/value lived in a ConditionalWeakTable keyed by
the managed UIElement. CsWinRT wrappers are collected and re-created for the
same native control across GCs, so entries vanished and a shallow-skip
re-publish (call site moved) wrote the value without |owner=. Measured: after
the benchmark's re-render series only 3,078 of 9,999 controls kept owner=
(1k: 306/990); with a forced GC before each re-render it showed after 2.
- No per-control state. Skip arms take the owner from the reconciler's current
owner (they run inside the parent's reconcile, like updates).
- The update path re-publishes only when the value can change:
IdentityChanged = call site, key, element kind or component type differ (or
the control was replaced). An ordinary re-render builds and writes nothing.
- The host root keeps its last value on the reconciler (one per host).
- Values are cached per shape (site, owner, kind, mounts, root, name, hooks):
key-free controls share one string; keyed ones are one concat. Format() is
split out and pinned byte-for-byte to the v1 grammar.
Effect (Native AOT, 50k nested, delta vs no-K, per control): managed heap
559 -> 372 B; mount allocations 1,403 -> 610 B; allocation per unchanged
re-render 1,158 -> 300 B; re-render with changed call sites 27 -> 10 us.
Switch-off (b) and switch-on/env-unset (c) builds remain within noise of the
baseline; the trimmer still removes all of it when the switch is off (mstat).
Tests: ReactorSourcePublisherTests (identity rules; cached Format equals the
direct Build for plain/keyed/escaped/no-site/no-owner/column-0/absolute
shapes; shared instance for unkeyed values); selftest skip-republish owner.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Source map: share the call-site-only extras bucket per call site
Stamping (REACTOR_SOURCEMAP=1, and diagnostics mode, which implies it) gave
every element a fresh ElementExtras bucket (~150 B) holding only its
CallSite, retained for every realized control. The bucket is an immutable
record, so an element whose only extra is its call site now takes a shared
instance cached per SourceLocation (bounded by the app's call sites; the cache
lives in a holder class, so an app that never stamps never allocates it).
Adding any behavioral extra still copies the bucket.
Effect (Native AOT, 50k nested, per control, delta vs no stamping):
stamping-only heap 354 -> 202 B; diagnostics mode heap 372 -> 220 B and
allocation per unchanged re-render 300 -> 142 B.
Docs: diagnostics guide states the measured cost of diagnostics mode.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* ReactorSource: allocate the value cache on first use
Every guarded reconciler call site reads ReactorSourcePublisher.IsEnabled, which runs the type's static initializer even when REACTOR_DIAGNOSTICS is unset (the Debug default). The value cache (a ConcurrentDictionary) was a static field there, so a Debug app with the switch on and publishing off retained ~14 KB it never used (measured: flat 1k heap after mount +1.9% vs baseline, now +0.0%). It now lives in a holder class initialized on first publish.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* ReactorSource: skip the call-site-only element tag under Native AOT
In diagnostics mode every stamped element was tagged (ReactorState + its
attached DP) only so a managed inspector could read its call site back. No
managed agent can load into a Native AOT process, so there the tag is pure
cost: measured ~0.2 KB managed and ~0.8 KB native per control.
- NeedsTag no longer tags an element whose only extra is its call site when
Reconciler.SkipsCallSiteOnlyTags: the Reactor.DevtoolsSupport switch,
REACTOR_DIAGNOSTICS=1, and no managed agent
(!RuntimeFeature.IsDynamicCodeSupported, decided once; settable for tests).
Such a control is in exactly the state an unstamped build leaves it, which
every functional tag reader already handles. Forced tags (decorators,
wrappers, dialogs, lists, Win2D, generated wrappers) and tags for
callbacks / keys / behavioural extras / reference modifiers are untouched.
- The spec-010 shallow-skip refresh allocates only via NeedsTag
(RefreshCallSiteTagOnSkip) instead of forcing a tag.
- ReactorSourceMap.GetSource resolves an untagged control from its published
value. Each value shape records its full SourceLocation against its at=
text, so it returns the same SourceLocation the tag gave. When two call
sites share the same at= text (same relative path from two assemblies),
that control is tagged after all, so GetSource stays exact.
- JIT, Debug, and builds without the switch are unchanged; the trimmer still
removes everything.
Measured (Native AOT, 50k controls, diagnostics on, per control vs no K):
managed heap 221 -> 20 B; private bytes 1,512 -> 713 B; mount allocations
458 -> 199 B; mount time 9.8 -> 3.3 us. Switch off / switch on with
REACTOR_DIAGNOSTICS unset stay within noise of the baseline.
Tests: a selftest runs one tree through both paths and requires GetSource to
agree on every control after mount and after a re-render that moves call
sites (43 controls, 30 located; 11 tags instead of 30, 19 resolved from the
published value). Mutation-checked: no fallback, or no republish on skip,
fails it. Unit tests: at= key parsing, published value -> full location,
ambiguity, mode predicate.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* ReactorSource: address review findings
- Security: only the SDK's PathMap roots (/_/, /_<digits>/) count as
deterministic, non-absolute paths. Any other /_... directory (/_work/...)
is absolute and is published as its file name with rel=0.
- ItemsRepeater adoption moves a fresh component subtree into the realized
wrapper; the wrapper now also takes the replacement's published value, so
it reports the new key.
- AOT GetSource fallback: a collision of at= texts no longer erases the first
mapping. Controls published after a collision keep their tag; an untagged
control with that text was published before it, for the first site.
- Tests: published name= and hooks= on live controls; adoption republish
(mutation-checked); /_work paths; collision keeps the first site.
- Docs: CHANGELOG entry; reactor-build-and-check skill documents
ReactorDiagnostics and the Debug default; unambiguous rel= in the
SourceProperty XML doc; threat model states the PathMap-root rule.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* ReactorSource: review round 2 fixes
- Security: a path rooted on the current drive (\Users\me\App.cs) is
absolute too; it is published as its file name with rel=0.
- Owner: the host's root owner is set only for its own render pass and
cleared after it. Rows an ItemsRepeater realizes or reuses during layout
run Reconcile outside any render, and used to pick up a stale root owner
(owner= is now omitted for them, as documented).
- Tests: rooted-path cases; a scroll selftest with keyed component rows
(pass-2 adoption during layout), mutation-checked (all 20 reused rows
reported the stale owner without the fix).
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* CHANGELOG: link issue #1341
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* ReactorSource: address Copilot review
- Generator: hooks are ordered by where the call ends, because C# evaluates
arguments first: in UseState(UseMemo(...)) the inner hook takes the
earlier slot. Regression test for a nested hook.
- A Memo(key, ...) host root now gets root= (and hooks=): the host adds the
root fields to the value its factory output published, without running
the factory again (ReactorSourcePublisher.WithRoot). Unit test (matches
Format for a root) and a selftest (factory ran once).
- The host-root publish keeps no reference to the root control. It compares
against the control's current value, so a disposed host's subtree is not
retained.
- Static source-map table: published before the pending marker is cleared
(volatile), so a lock-free reader never sees a cleared marker with the old
or missing table.
- Reactor.targets: a final ReactorDiagnostics=false turns the switch off even
over a project-declared Reactor.DevtoolsSupport=true (scaffolded Debug
apps) or when set in the project body after the import. This mirrors
-p:ReactorDiagnostics=true beating a project-declared false. Targets tests
cover both import orders (mutation-checked).
- Docs: recompiled architecture-overview (its ElementFactory snippet moved)
and diagnostics; the build-and-check skill notes the opt-out precedence.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Address Copilot review round 2 on ReactorSource
- Reactor.targets: a ReactorDiagnostics=true set in the project body after an
early import now also implies ReactorSourceMap (late target + analyzer item
condition evaluated against the final values).
- Hook names: the generator records every Render() override (hook-free ones
too), and GetComponentHooks walks base types, so an inherited Render reports
the declaring base's hooks instead of nothing.
- Hook tables are keyed by (assembly, full name), so equal type names in
different assemblies no longer collide; RegisterStaticInfo takes the assembly.
- Pool return and detach clear the published ReactorSource value, so the
Native AOT GetSource fallback cannot report a recycled control's old element.
- diagnostics doc links the threat model by absolute URL (mkdocs --strict).
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Address Copilot review round 3 on ReactorSource
- Reactor.targets: the Debug default no longer writes ReactorDiagnostics at
evaluation, so a project-body ReactorDiagnostics=true after an early import
stays an explicit opt-in that beats a project-declared switch-off; items
apply the default and _ReactorApplyLateDiagnosticsOptIn fills the property.
- Hook slots: component-extension hooks (this.UseElementFocus()) are hooks;
abstract, overridable and body-less custom hooks are an unknown slot count.
- Root swap: mounting a different root over kept content rewrites owner= on the
root-owned controls the in-place update did not republish (both hosts).
- Static info: a location two assemblies claim with different names or
render-function hooks is unknown instead of last-writer-wins.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Address Copilot review round 4 on ReactorSource
- Native AOT tag skip keeps the tag of a stamped element whose V1 handler tears
down child slots the generic unmount walk does not reach (NavigationView pane
slots, item hosts): unmount dispatches to that handler through the tag.
- ReturnControl clears the published value even when the pool declines the
control (pooling disabled), so GetSource's fallback forgets it too.
- Static info: one fingerprint per source file over every call site's name or
its absence and every render function's hooks or their absence; a path two
assemblies stamp with different fingerprints has no attributable facts.
- Root swap: the owner rename also walks flyout content / pass-through, popup
children, a showing dialog's content and cached navigation pages.
- Keyed-memo roots: hooks= the host added on an earlier pass is replaced or
dropped when a different root is mounted.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Address Copilot review round 5 on ReactorSource
- Native AOT tag skip reads the handler's ChildrenForUnmount (descriptor item
hosts and items-binder strategies are hidden from Children), so a stamped,
callback-free ComboBox / Pivot with component items keeps its teardown tag.
- Root-owner rename visits a node's overlay edges before stopping at a
component wrapper (Flyout(Component<T>(), body)), and walks logical children
too (generic content walk, the handler's live strategy children, items, tabs),
deduplicated, so unrealized template content such as a closed flyout's
Button content is renamed.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Gate the new ReactorSource fixtures on what the host provides
The AOT and packaged selftest hosts are not source-mapped: nothing is stamped
(so the tag-skip teardown scenario does not exist there) and there is no
static hook table (so a keyed-memo root has no hooks to replace). Skip those
checks there, as the existing both-paths fixture already does.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Address Copilot review round 6 on ReactorSource
- Root-owner rename stops at an embedded ReactorHostControl: its island is
another host and names its own root.
- Keyed-memo roots: hooks the host added before are only removed from a value
that still carries the host's root fields, never from a fresh output whose
own hooks happen to match.
- ContentDialog: the visible WinUI dialog carries the element's published
value too (mirrored when the placeholder is published, copied when a dialog
is created later), and the rename walk renames it.
- Native AOT tag skip keeps the tag for any stamped element whose handler is
hand-written (its Unmount may hold imperative teardown); descriptor handlers
are unaffected. OwnsChildTeardown is now OwnsTeardown.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Address Copilot review round 7 on ReactorSource
- Root publication unwraps ModifiedElement before its kind checks, so a
modifier-wrapped keyed-memo root keeps describing its realized output.
- Both root-publication paths (plain and keyed-memo) mirror the final value
onto a live ContentDialog, so the visible dialog follows a root swap; the
mirror keys on the placeholder's live dialog, not on re-running a factory.
- ReactorSourceMap.Enabled starts on under the diagnostics launch opt-in
(REACTOR_DIAGNOSTICS=1 in a build with Reactor.DevtoolsSupport), so elements
built before the first host, such as a page's field initializers, are
stamped. The read stays behind the feature switch.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* docs: diagnostics launch opt-in also starts source mapping
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Root-owner rename never enters an embedded host, even as the root's content
A root that returns XamlHost(() => island) makes the island its content
control; the rename walk now stops at any ReactorHostControl after renaming it
and visiting its decorator-owned overlays, so the island's own annotations are
never rewritten.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Address Copilot review round 9 on ReactorSource
- A per-host RegisterType callback that forwards to a child returns the
child's control: its published value stays the child's on mount and update
(decided from the value, so it holds where neither element is tagged), and
Native AOT GetSource matches JIT.
- A ContentDialog body mounted outside a render (a deferred Loaded open) is
mounted under the owner its placeholder was published with.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Forwarding RegisterType roots keep the child's source fields
A host root that dispatches to a per-host RegisterType registration (no V1
handler for the type, as in Mount) and returns a control describing another
element gets only the host-root fields added (WithRoot), like a keyed-memo
root, so Native AOT GetSource matches JIT.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Address Copilot review round 11 on ReactorSource
- Forwarding RegisterType: the child's published value is kept only when the
child holds (or, stamped, would hold) the JIT tag; for an unstamped,
untagged child the registration's location is published, as JIT's
registration tag reports it.
- Root-owner rename also walks context-flyout and attached-flyout content and
a Reactor-mounted tooltip, before stopping at component or host boundaries.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* GetSource names a forwarded child when a forwarding registration tags itself
In the Native AOT skip mode a forwarding RegisterType registration that needs
a tag (a key, an unmount callback) tags the control with itself because the
stamped child is untagged; JIT keeps the child's tag. GetSource now answers
from the published value (the child's) when it describes another element
than the tag, matching JIT.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Isolate the process-wide Reactor.DevtoolsSupport switch in tests
- TestSetup captures the configured switch and initializes the cached gates
(ReactorFeatures.DevtoolsSupported, ReactorSourcePublisher.IsEnabled,
ReactorSourceMap.Enabled) before any test runs.
- Tests that flip the live switch restore the configured value instead of
leaving it off.
- Diagnostics tests compare against the captured value; the fresh-load
startup test sets the switch explicitly for each load (adding the
switch-off cases) and runs in the same collection as the switch mutators.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Root-owner rename walks RichTextBlock inline UI children
Reactor-managed (Route A) InlineUIContainer children live in a RichTextBlock's
document blocks, not its visual tree, until it is realized; the rename walk
now visits the same set UnmountInlineUIChildren tears down.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Report propagated root failures; time root throws before logging; docs
- Both hosts report a root Render() left by an exception the app asked to
propagate (no fallback is shown, but the render ran), then rethrow.
- A root throw is timed at catch entry, before any debugger break, logging,
or hook-order recovery work.
- ReactorTrace.GetComponentControl documents every case in which an id does
not resolve (yet).
- CHANGELOG records the host root lifecycle fix under Fixed.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Name the root on an app-supplied render-error fallback
When a root throws and the app's RenderErrorHandler supplies a fallback, both
hosts install it without root publication. The fallback's content root (its
internal guard) now gets the host-root fields (root=, the root's hooks=) added
to its own value, like a keyed-memo root; the fallback's controls keep their
own attribution.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Finish root retirement even when a cleanup throws
RetireRoot runs the outgoing root's cleanups through RenderErrorDispatch,
like Dispose (issue #1291): with a RenderErrorHandler every failure is
reported and the rest still run; with none the first escapes. Either way
the retirement completes (hook state cleared without re-running cleanups,
slots cleared, diagnostics reset) and Mount installs the new root before
rethrowing, so a failing cleanup can neither block the replacement nor stay
armed for the next Mount. The propagated-root fixture owns its nested window
and hosts through a disposable scope.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Catch only the expected exception type in the cleanup-failure fixture
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Drop stale static facts after hot reload or a late registration
- A hot-reload update makes every declared name and hook list unknown
(the tables describe the compiled build; an edit can move a call onto a
location the table gives another element).
- A source-mapped assembly registered after the table was built bumps a
facts revision (it can make another assembly's file unattributable).
- On its next render each host walks its tree once per revision change and
drops name=/hooks= fields that no longer hold, including on controls that
were not re-published or were skipped.
- The selftest runner restores the compiled tables after each fixture, since
hot-reload fixtures invalidate them process-wide.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Release the replaced root's tree when the new root renders nothing
After a root swap the previous root's reconciled tree stays on screen until
the replacement renders. If that first render returns null, nothing
reconciled it away, so its components (and their effects) stayed live. Both
hosts now release it in that case: the old tree is reconciled to null and no
content is shown. A root that merely re-renders to null keeps its content.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Flush a root's effects when it renders null
A root whose Render() returns null still commits: both hosts now flush the
root context's effects on that path too (failures routed as Effects), so a
root UseEffect runs and has a cleanup to retire later.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Address review: exact sites for stale facts; hot reload invalidates regardless of switch
- The stale-facts refresh checks each control's exact call site (its tag where
it has one), so when two files share one at= text only the stale one loses
its name=/hooks=.
- HotReloadService invalidates the static facts without the devtools switch
guard: SourceLocation.DeclaredName reads the same tables in any
source-mapped build.
- Fix a Release-only nullable warning in the stale-facts fixture.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Performance guide: the provider has fifteen keywords, not seven
The regenerated keyword table lists fifteen (including RenderDetail); the
prose now says so, keeps the seven render-loop keywords' descriptions, and
points to Perf instrumentation and Diagnostics for the rest.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Shallow-skip tag refresh honours handler-owned teardown
An unstamped, callback-free item host that gains its call site through a
shallow skip (root, positional or mixed-key) now gets the tag its handler's
teardown is dispatched through in the Native AOT skip mode, as on mount and
update. RefreshCallSiteTagOnSkip is an instance method consulting the
handler (IV1TeardownOwner).
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Release the replaced root's tree when the replacement fails
A replacement root that fails before committing took the built-in panel
path, which abandons the current tree unless it is an app fallback; the
previous root's descendants then stayed live off-screen. A pending root
replacement now counts as a tree to release in both hosts' ShowErrorFallback.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Finish releasing a replaced root's tree when a cleanup throws
ReleaseReplacedTree now tears the old tree down under
IsolateUnmountCleanupFailures, like a render-error release: every cleanup
runs, failures are logged, and a requested propagation is rethrown after the
release. The pending-replacement marker is cleared only once the release has
succeeded, so a failure still routes the outer render-error path to release
the tree instead of abandoning it.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Root-owner and stale-facts walks reach TreeView node content
Legacy TreeViewNodeData.ContentElement controls are mounted for every node,
collapsed ones included, and sit in no visual or logical child walk; the
shared publish walk now visits TreeView.RootNodes recursively.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Route a replaced tree's cleanup failures like disposal's
ReleaseReplacedTree still tears the whole old tree down under
IsolateUnmountCleanupFailures, but now collects the failures and, once the
release is done, routes them through the new
RenderErrorDispatch.ReportCleanupFailures: each is reported to the
RenderErrorHandler as Cleanup; with no handler the first is rethrown (and
surfaces through the render loop's error path) instead of only being logged.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Stamp the column on root mount sites
ReactorHostInfo.MountSite (#1318) was recorded as file + line only; with
SourceLocation.ColumnNumber available, the root-mount interceptors now pass
the column too.
- ReactorSourceMap.EnterRootMountSite(string filePath, int lineNumber, int
columnNumber) replaces the 2-arg overload (unreleased infrastructure API,
EditorBrowsable.Never; the generator in the same package is its only
caller).
- The generator stamps the method name's column for ReactorApp.Run /
OpenWindow, ReactorWindow.Mount and ReactorHost / ReactorHostControl.Mount,
using the same CallSiteColumn rule as element call sites. RootMountSite
model equality includes the column.
- Tests: generator driver (same-line Run/Mount calls, split name/paren),
live RootMountInterceptionTests column oracle against the test file's text,
and a runtime pass-through test. Source-mapping guide updated.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Use Lock for the host and render-trace gates; update host diagnostics fixture for render remounts
IDE0330 (enabled by #1351) rejects object lock gates in Release. The HostDiag fixture from #1318 asserted a render-function remount on a component root was ignored; this branch makes it replace the root.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Detach retired root contexts from their host; unmap a replacement adopted while tracing is off
A retired root's RenderContext kept its rerender callback and context scope, so a retained instance or hook setter pinned the host and re-rendered the replacement root. TryAdoptRealizedReplacement left the id on the discarded replacement wrapper when the listener was disabled mid-adoption.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Make SourceLocation.ColumnNumber a required positional member
Drops the extra constructor and init-only property; every call site now passes the column (0 when unknown).
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Drain every root cleanup on retirement; route a swapped-out tree's cleanup failures to the handler
With no RenderErrorHandler, a throwing root cleanup stopped the rest, which retirement then dropped. Cleanup failures in a swapped-out root's tree released by a throwing replacement were only logged. Also time a child hook-order retry after the reset and before logging, add a ReactorHostControl root-swap fixture, and consolidate the duplicated ReactorSourceMap row in the packed skill.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* ReactorSource: root render-function hooks via the mount site; shared component names
Folds in the integration follow-up (2495eb4b7) now that #1318 and #1323 are
on main: a root render function's hooks= resolves from its recorded mount
site, and owner/mounts/root names forward to ComponentNames.For. Also adopts
System.Threading.Lock for the static-facts gate (IDE0330, now enforced in
Release).
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Report a root that mounts its replacement mid-render as itself; recheck the gate after mount; resolve the released tree's handler per failure
The hosts capture the root and its diagnostics at render entry and replace (not reset) the diagnostics on retirement, so an attempt that called Mount keeps its name and id and the replacement's first render is its own mount. TrackMountedComponent rechecks ComponentRenderTrace.IsEnabled (a listener may dispose itself in the callback). ReportReleasedTreeCleanupFailures resolves the handler for each failure and logs one that has none.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Dedupe the build-properties table; skip the root hooks check where call sites are not stamped
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Defer re-entrant root retirement to the next pass; recheck the trace gate before mapping a re-rendered wrapper
Mount called from the outgoing root's own Render() or effect no longer clears and detaches contexts that code is still running against: the retirement is queued and runs at the start of the next pass (or on Dispose), so an effect's returned cleanup is not orphaned. EmitComponentRendered rechecks ComponentRenderTrace.IsEnabled before mapping, covering a listener disposed on a hook-order retry's aborted attempt.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Keep the teardown tag for single-content strategies in the AOT tag skip
A single-content strategy writes its child through an opaque setter, so the
generic unmount walk is not proven to reach it (a ContentControl that keeps its
child outside Content). Only a panel strategy whose collection is the control's
own Panel.Children is exempt now. Adds a live custom-slot regression in both
tag modes.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Retire every deferred root; snapshot the rendering root after deferred retirement
RetireDeferredRoots skipped the rest of the queue after the first failure (??= short-circuits). The rendering-root snapshot is now taken after deferred retirement, so a root mounted by a deferred cleanup is reported as itself.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Time ReactorHost's root ComponentRendered from the root's Render() call
The PERF tree-phase stopwatch also covers the charting-state push, which is not part of the root's render.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Detach deferred roots on Dispose
Dispose cleaned up roots queued for deferred retirement but did not detach their contexts, so a retained old root or setter could pin the disposed host. Route them through RetireContexts.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Publish the root a pass renders, not the live root, in both hosts
A child can Mount() a replacement root during reconciliation. The pass still
renders the outgoing root, so its root value, owner and hooks now come from a
snapshot taken with _renderingRoot (plus the mount site), and the replacement's
next pass renames the retained descendants. Adds a reentrant-remount regression
for ReactorHost and ReactorHostControl.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Keep the pending tree release when a root is replaced mid-pass
A root that mounts its replacement from its own Render() still commits its tree (or error panel) in that pass. That content now stays marked for release, so a replacement whose first render is null removes it instead of leaving it and its effects live.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Record the mount site when the active root instance is mounted again
Re-mounting the current root keeps it and its effects, but the diagnostics site now follows the latest explicit call (as on main).
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Name a null-returning root's effect failure after the root
The null-tree path flushed the class root's effects without capturing effectsRoot, so a failure was reported as FuncElement with no component name.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Include root mount sites in the static-info file fingerprints
A file whose only Reactor call is a hook-free root mount (output built with
constructors) made no b.Source claim, so another assembly's hook-using root
at the same path, line and column lent it its hooks. Every root mount site now
records its (possibly empty) render-function hooks fact. Generator regression
plus docs.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Report every render of a hot-reload pass as hotReload
ReactorHostControl's hot-reload passes are not forced, so a child reached from its parent reported props/context/parent. ClassifyUpdate now gives an open hot-reload pass precedence, and the hook-order retry marker is independent of the force flag (both hosts set it) and dropped when its pass ends.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Regenerate reconciler snippets; scope hotReload to re-renders
The architecture-overview and reconciliation guides embed the reconciler-entry snippet, which still named the old retry fields. A component first created during a hot-reload pass is a new instance and reports mount; the docs now say so.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Drop stale static facts from content a null-rendering root keeps
Both hosts returned before any publication when the root rendered null, so
content it kept held obsolete name=/hooks= after a hot-reload update or a late
conflicting registration. The null-tree branch now runs the reconciler's
stale-fact refresh on the retained content, under the diagnostics gates.
Non-null to null regression for ReactorHost and ReactorHostControl.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Keep a root mounted by the outgoing root's cleanup
RetireRoot clears the root slots before running the outgoing cleanups, and Mount bumps a generation: when a cleanup mounted another root, the outer Mount leaves it in place instead of retiring and overwriting it, matching the deferred path.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Re-describe a null-rendering root's retained content, host hooks included
A function root's hooks= is added by the host from its mount site, so the
stale-fact walk alone could not drop it after a late conflicting registration.
A null render now re-publishes the retained root as a publishing pass would
(PublishRootSource, PublishFallbackRootSource for an app fallback, or the
stale-fact walk for the built-in error panel). Late-conflict null-render
regression for both hosts.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Resolve target-wrapping decorators before the publish walk's child enumeration
A Flyout replaces its target's tag with FlyoutElement, whose adapter enumerates
no children, so a decorated named-slot host (Flyout(NavigationView(...), ...))
left an unrealized PaneHeader outside root-owner renames and stale-fact
invalidation. The walk now unwraps decorators (ReactorSourceMap.UnwrapDecorators,
cycle-safe) first. Live regression in ReactorSource_RootRemountRenamesOwner.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Enumerate panel-strategy collections in the publish walk's live-child visit
A panel strategy's collection can belong to an inner panel the host's template
has not attached, outside the visual and logical walks, so root renames and
stale-fact invalidation missed its children. VisitLiveChildren now visits it.
Live regression with a custom untemplated host.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Document that a host root's componentId only maps forward
Cherry-picked from 722b41ec6, #1328's post-merge doc fix (pushed after the
squash merge, so not on main).
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Address github-code-quality notes on ReactorSource
Combine the nested hot-reload check in WithoutStaleFacts, filter the three
diagnostics-only loops (navigation cache snapshot, inline UI paragraphs,
longest-prefix root) up front, and make the keyed-memo fixture's factory
counter a local.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Filter up front in the source-map generator's static-info loops
Follow-up code-quality notes: Where/Select instead of skip-and-continue in the
generator's fact, hook-slot and destructuring loops, and MaxBy for the longest
root prefix. No behaviour change (generator snapshot and root-relative tests).
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Describe a decorated control by its target's call site only
A stamped decorator over an unstamped target published the decorator's
location as the target control's at= (and name=), while GetSource reports no
location for it. Format and IdentityChanged now take the call site from the
unwrapped target alone, exactly as GetSource does. Unit test plus a live
mount/update fixture. Also: code-quality notes (Select in the generator's
hook-slot walk, a documented best-effort catch, Path.Join in two tests).
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Gate ReactorSource stale-fact fixtures on an independent stamping probe
The null-render, late-conflict and stale-facts fixtures decided whether the
host stamps call sites from the published value under test, so a host that
stopped publishing at= skipped (and the HostControl path ignored the result)
instead of failing. They now probe a factory call's CallSite with source
mapping on, and once stamping is established require at= with H.Check on every
host path.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Let a source-mapped plug-in in a collectible load context unload
ReactorSource diagnostics kept a collectible AssemblyLoadContext alive when the
plug-in shares the host's Reactor:
- the pending static-info registrations (assembly + fill callback) and the
built table's assembly set and component-hook keys held the assembly
strongly. Both are now owned weakly by the assembly (ConditionalWeakTable);
location-keyed tables hold strings only;
- the published-value cache keyed shapes by element Type; it now keys by the
formatted kind string, cached per type in a ConditionalWeakTable;
- ControlRegistry's source-target and base-dispatch caches, which every
published control goes through, no longer cache collectible types.
Unload regression tests (before and after the first table lookup, and after
formatting a plug-in element), each mutation-checked.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Clear a closed ContentDialog's published ReactorSource
The dialog's close cleanup cleared only the element pointer, so a dialog an app
keeps through .Set(...) still carried its ReactorSource, which Native AOT's
GetSource falls back to (JIT already returned null). It now uses
ClearElementTag, which also clears the published value. Live check in both tag
modes.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
ReactorEventSource.RenderError(EventId 9) reported the element type for class components —ComponentElement`1for everyComponent<T, TProps>(…),ComponentElementforComponent<T>()— sowinapp devtools logsand the overlay error badge could not name<Counter>. Mount-time and host-root render errors emitted no event at all, and neither did any render error anErrorBoundarycaught. This PR reports the component's type name on every path where a component'sRender()throws, including inside a boundary, with exactly one event per throw.Linked issue / spec
Fixes #1321
Spec 044 §6.2.1 (PII) is respected: payloads carry developer-authored type names only.
What changed
ComponentNames.For(Component? instance, Element? element)(internal,src/Reactor/Core/Diagnostics/ComponentNames.cs), precedence: the live instance's type →ComponentElement.ComponentType→ the element type (FuncElement/MemoElement). Non-generic names are exactlyType.Name, as before; a generic component renders asItemList<Int32>instead ofItemList`1. A nested generic shows only its own type arguments. Reflects onlyName/GetGenericArguments, so it is AOT/trim-safe. Generic names are cached per type in aConditionalWeakTable, so collectible or hot-reloaded types are never rooted.RenderErroruses that name, and is now emitted from:Renderkeyword being on;Component,Func,Memo(new);ReactorHostandReactorHostControl(new), and those hosts' outer catch when the root's effect flush throws (new).Reconcile/Cleanupfailures have no failing component and are not reported;ErrorBoundary(new): each component catch gains a sibling clause,when (_errorBoundaryDepth > 0), that emitsRenderErrorand rethrows unchanged. The throw site is the only place that knows which descendant threw, and the boundary does not emit again, so there is exactly one event per throw.Still
RenderError(EventId 9,Errorskeyword,Errorlevel, same payload). Existing subscribers pick up the new emissions with no change.ComponentRenderStart/Stop,EffectsFlush*,ComponentUnmount(including theErrorBoundaryrollback unmount from [Feature] Let apps replace the built-in error fallback that shows the full exception text #1291) and the reconciler'sILoggermessages use the same helper, so every event agrees on the name.ComponentUnmountnow computes the name only when itsLifecyclekeyword is enabled.SwallowedErroris unchanged (its payload is category / operation / exception type; no call site passes a component).Guide:
source-mapping(template + compiled + diagram) — the component-name paragraph, the attribution diagram label/alt text, and theRenderErrorrow.CHANGELOG.md[Unreleased]→Fixedentry.advancedguide: how the ETW event relates toRenderErrorHandler. Spec-044 swallowed-error ledger rows for every render-error catch this PR touches.Test plan
tests/Reactor.Tests/Diagnostics/ComponentNamesTests.cs(12). Mutation check: reverting to element naming / rawType.Namefails 4 of the original 9;GenericName_IsBuiltOnceAndReusedcovers the per-type name cache.tests/Reactor.AppTests.Host/SelfTest/Fixtures/RenderErrorNamesFixture.cs, subscribed toErrorsonly:RenderErrorNames_ComponentTypeOnEveryPath— class/func/memo mount, update,ReactorHostroot component + render function,ReactorHostControlroot component + render function.RenderErrorNames_ErrorBoundaryCatchIsReported— inside a boundary on mount (class, func, memo, and a throw that crosses an intermediate component) and on update: fallback shown, reported exactly once, by the thrower's name. Also an exception whoseMessagegetter throws, and an explicitly thrownOutOfMemoryExceptionon every boundary arm (class/func/memo mount and update).dotnet run --project tests/Reactor.AppTests.Host --no-build -c Debug -p:Platform=x64 -- --self-test --filter "RenderError"— passed, 0 failures (at the merge4075055bf), together with main'sRenderErrorHandler_*andErrorBoundary*fixtures.FuncElementmount emission and theReactorHostControlroot-function emission failsRenderErrorNames_Mount_FuncComponentReportedandRenderErrorNames_HostControl_RootFunc_Reported(2 failures); readingex.MessageinEmitRenderErrorfailsRenderErrorNames_Boundary_ThrowingMessage_ReportedOnce; restoring the OOM/SO carve-out on the boundary arms fails all 4 boundary*OutOfMemory_ReportedOncechecks; limiting the report-and-rethrow arms to boundaries again fails the 4 non-boundary OOM checks. Restored afterwards.dotnet test tests/Reactor.Tests -p:Platform=x64— passed: 14435 total, 14371 succeeded, 64 skipped, 0 failed (atd8496be71, onorigin/main1e8c6b196).dotnet test tests/Reactor.DocPipeline.Tests -p:Platform=x64— passed 479/479.SwallowedErrorAuditTests— passed 11/11.dotnet build src/Reactor/Reactor.csproj --no-restore -c Release -p:Platform=x64anddotnet build tests/Reactor.AppTests.Host --no-restore -c Release -p:Platform=x64— passed, 0 errors (warnings-as-errors on). The fulldotnet build Reactor.slnx --no-restore -c Releasecould not be completed locally:dotnet restore Reactor.slnxfails withNU1301(TLS handshake to nuget.org) for thevs-reactorprojects in this environment; relying on CI'sBuild solutionjob for that gate.REACTOR_SELFTEST_SHARD=1/2 dotnet test tests/Reactor.SelfTests -p:Platform=x64at78a593d2e— passed: 930 total, 929 succeeded, 1 skipped (the intentionalSelfTestVerdict_OnlySkips_PositiveControl), 0 failed.reactor.api.txtunchanged.origin/main(00cd48920: Let apps replace the built-in render-error fallback #1311RenderErrorHandler, Add pr-lifecycle contributor skill #1346). Conflicts at every render catch site were resolved keeping both intents. Main'sBuildInTreeFallback/ShowErrorFallbackdispatch and!RenderErrorDispatch.IsPropagating(ex)filters are unchanged.RenderErroris emitted before the handler runs, so it fires once per throw whatever the handler returns. The report-and-rethrow arms also let a declined propagation pass, since its throw site already reported it. New fixtureRenderErrorNames_RenderErrorHandlerOutcomesReportOncecovers an app fallback (child and root), a handledPropagate(), and a declinedPropagate()escaping through anErrorBoundary; each is reported once, by name. Mutation: dropping theIsPropagatingfilter reports the declined case twice (FuncElement, NestedDecliningComponent).RenderError.ComponentName(the handler API) keeps main's values. API index regenerated withmur --regen-api: unchanged. Copilot's round on the merge raised 3 findings, fixed in9ee41eaa4. (1) The [Feature] Let apps replace the built-in error fallback that shows the full exception text #1291 rollbackComponentUnmountused the old naming; it now goes throughComponentNames+ the keyword guard, covered byRenderErrorNames_BoundaryRollbackUnmountNamesComponent. (2, 3) Host-root effect failures reached the handler with noRenderError; they now emit, covered byRenderErrorNames_*RootEffect*/*RootFuncEffect*. All 5 new checks fail with the fixes reverted. The docs scope the event to componentRender()/effect failures, and the spec-044 ledger records the hosts' outer catches (LedgerSiteTotal136 → 138). The next round, fixed in0469209dc: (a) both hosts read_rootComponentafter app code ran, so a root thatMount()s a replacement and then throws was attributed to the replacement. The root is now captured beforeRender()/FlushEffects(), and the same instance names the event and the handler'sRenderError.ComponentName; covered byRenderErrorNames_*RootReplaced*(4 checks, all red on the mutant). (b) "Previously missed":ComponentNamescut an array argument down to its generic name (GenericList<List<int>[]>→GenericList<List>); it now formats element type and rank, covered by the unit testArrayArguments_KeepTheirElementTypeAndRank(red on the mutant). Mergedorigin/mainagain (fix(datagrid): keep inline editor text aligned with the display cell #1349, Enable IDE0330 and move lock gates to System.Threading.Lock #1351): the CHANGELOG conflict was resolved by keeping bothFixedentries. The next round found that the func/memo mount arms hardcodedFuncElement/MemoElement, while those records are non-sealed and update/unmount name derived records by their own type. Fixed by routing them throughComponentNames.For, covered byRenderErrorNames_Mount_Derived*ElementNamed(red on the mutant). Release builds ofsrc/Reactor,tests/Reactor.AppTests.Hostandtests/Reactor.Testsare clean under the newly enabled IDE0330. The next round asked for generic derived element records to use the generic format (DerivedFunc<Int32>, notDerivedFunc1). Fixed inb585258and covered by the unit testGenericDerivedElements_UseTheGenericFormat(red on the mutant). Mergedorigin/mainagain (#1347, #1318) with no conflicts. #1318's new publicReactorHostInfo.RootComponentName` deliberately uses its own fully qualified format and is untouched.main. At22f267926, main's CI fails onsrc/Reactor/Core/Diagnostics/ReactorDiagnostics.Hosts.cs(305):error IDE0330. Diagnostics: enumerate Reactor hosts and their roots; tag component boundaries for inspectors #1318 addedReactorHostRegistry'sobjectgate after Enable IDE0330 and move lock gates to System.Threading.Lock #1351 enabled IDE0330. Commit3cc699958makes it aSystem.Threading.Lock, the same change Enable IDE0330 and move lock gates to System.Threading.Lock #1351 made to every other gate. It's unrelated to this PR. Main then fixed the same break in Fix IDE0330 build break in ReactorHostRegistry #1356; the next merge (671fed2dc) took main's version of that file, so the one-liner merged away. After that merge: the API index was regenerated (unchanged), and the onlylockstatements this PR adds are test-local locks onListinstances, notobjectgates, so IDE0330 is clean.dotnet test tests/Reactor.Tests -p:Platform=x64passed 14579 total, 0 failed, 64 skipped (at3cc699958). Release builds ofsrc/Reactor,tests/Reactor.AppTests.Hostandtests/Reactor.Testsare clean, and theRenderError/ErrorBoundary/HostDiagnosticsselftests have 0 failures.dotnet test tests/Reactor.DocPipeline.Tests -p:Platform=x64passed 493/493. Releasesrc/Reactorbuilds clean.dotnet test tests/Reactor.SelfTests -p:Platform=x64(full): 1785 total, 1777 passed, 5 skipped, 3 failed. The failures areIssue487_ScrollViewOffsetRestoredAfterRunMutation,ItemsView_Parking_PendingBringThenKeyedResetandItemsView_Parking_VariableHeightReset; a cleanorigin/main00cd48920worktree fails them identically on this machine, so they are environmental and unrelated to this PR. CI is the gate.RenderErrorleaksex.Message— not the case, theReactorEventSource.RenderErrorsink writesstring.Emptyfor the message (TASK-064); replied and noted it onEmitRenderError. One asked to cache generic names — done in78a593d2e(ConditionalWeakTableper type), and bothComponentUnmountcall sites now skip computing the name when theLifecyclekeyword is off. Round 2: "Previously missed" — CHANGELOG entry; added under[Unreleased]→Fixed. Round 3: the new catches were missing from the spec-044 swallowed-error ledger (docs/specs/044/swallowed-error-audit.md); recorded 12 sites (8Keep/ user-callback isolation fallback catches incl. the host roots, 4PromoteEventreport-and-rethrow arms), distribution re-derived,LedgerSiteTotal124 → 136. Round 4: don't read the virtualex.Messageon the error path (a throwing override would replace the original exception) — fixed, selftest added. Round 5 "Previously missed": the boundary arms' OOM/SO carve-out let an explicitly thrownOutOfMemoryExceptionbe recovered silently — carve-out removed (the boundary's own catch takes everything), selftests added; and the source-mapping diagram label + alt text still saidComponent.GetType().Name— updated in template, guide and both SVG copies. Round 6 "Previously missed": outside a boundary an explicitly thrown OOM/SO skipped the fallback arm and was recovered by the host's outer catch without an event — the report-and-rethrow arm now takes OOM/SO anywhere (they still propagate exactly as before); selftests cover class/func/memo mount and update. Round 7: Findings: None, nothing previously missed.pr-reviewskill: 0 critical / 0 high. Four medium test-coverage gaps (func/memo mount,ReactorHostroot render function,ReactorHostControlroots) fixed by the third commit. One low finding dismissed: "ComponentNames.For(Type)duplicatesReconciler.Mount.FriendlyTypeName" —FriendlyTypeNameprints the declaring chain (Outer.Inner<T>) for copy-pasteable handler diagnostics, while component event names deliberately keepType.Namefor nested types so existing trace names stay byte-identical; sharing would need a mode switch that changes one or the other. Multi-model cross-check agreed.Risk / breaking changes
RenderErroralso fires for render errors at mount time (class, function, memo), in the host root (ReactorHostandReactorHostControl), and inside anErrorBoundary(once, at the throw site, even though the app recovered and shows the fallback). Errors-keyword consumers will see more events.ComponentElement/ComponentElement`1to the component's type name. That is the fix, but greps keyed on the old string will stop matching.Foo`1toFoo<Int32>in all component events. Non-generic names are byte-identical.ComponentRenderedevent change should use the sameComponentNameshelper; that will be folded into whichever of the two lands second.